﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace System.Text.Json.Nodes
{
    [DebuggerDisplay("{ToJsonString(),nq}")]
    [DebuggerTypeProxy(typeof(JsonValue<>.DebugView))]
    internal abstract class JsonValue<TValue> : JsonValue
    {
        internal readonly TValue Value; // keep as a field for direct access to avoid copies

        protected JsonValue(TValue value, JsonNodeOptions? options = null) : base(options)
        {
            Debug.Assert(value != null);
            Debug.Assert(value is not JsonElement or JsonElement { ValueKind: not JsonValueKind.Null });

            if (value is JsonNode)
            {
                ThrowHelper.ThrowArgumentException_NodeValueNotAllowed(nameof(value));
            }

            Value = value;
        }

        public override T GetValue<T>()
        {
            // If no conversion is needed, just return the raw value.
            if (Value is T returnValue)
            {
                return returnValue;
            }

            if (Value is JsonElement)
            {
                return ConvertJsonElement<T>();
            }

            // Currently we do not support other conversions.
            // Generics (and also boxing) do not support standard cast operators say from 'long' to 'int',
            //  so attempting to cast here would throw InvalidCastException.
            throw new InvalidOperationException(SR.Format(SR.NodeUnableToConvert, Value!.GetType(), typeof(T)));
        }

        public override bool TryGetValue<T>([NotNullWhen(true)] out T value)
        {
            // If no conversion is needed, just return the raw value.
            if (Value is T returnValue)
            {
                value = returnValue;
                return true;
            }

            if (Value is JsonElement)
            {
                return TryConvertJsonElement<T>(out value);
            }

            // Currently we do not support other conversions.
            // Generics (and also boxing) do not support standard cast operators say from 'long' to 'int',
            //  so attempting to cast here would throw InvalidCastException.
            value = default!;
            return false;
        }

        internal sealed override JsonValueKind GetValueKindCore()
        {
            if (Value is JsonElement element)
            {
                return element.ValueKind;
            }

            Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(default, JsonSerializerOptions.BufferSizeDefault, out PooledByteBufferWriter output);
            try
            {
                WriteTo(writer);
                writer.Flush();
                return JsonElement.ParseValue(output.WrittenMemory.Span, options: default).ValueKind;
            }
            finally
            {
                Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, output);
            }
        }

        internal sealed override bool DeepEqualsCore(JsonNode? otherNode)
        {
            if (otherNode is null)
            {
                return false;
            }

            if (Value is JsonElement thisElement && otherNode is JsonValue<JsonElement> { Value: JsonElement otherElement })
            {
                if (thisElement.ValueKind != otherElement.ValueKind)
                {
                    return false;
                }

                switch (thisElement.ValueKind)
                {
                    case JsonValueKind.Null:
                    case JsonValueKind.True:
                    case JsonValueKind.False:
                        return true;

                    case JsonValueKind.String:
                        return thisElement.ValueEquals(otherElement.GetString());
                    case JsonValueKind.Number:
                        return thisElement.GetRawValue().Span.SequenceEqual(otherElement.GetRawValue().Span);
                    default:
                        Debug.Fail("Object and Array JsonElements cannot be contained in JsonValue.");
                        return false;
                }
            }

            using PooledByteBufferWriter thisOutput = WriteToPooledBuffer(this);
            using PooledByteBufferWriter otherOutput = WriteToPooledBuffer(otherNode);
            return thisOutput.WrittenMemory.Span.SequenceEqual(otherOutput.WrittenMemory.Span);

            static PooledByteBufferWriter WriteToPooledBuffer(
                JsonNode node,
                JsonSerializerOptions? options = null,
                JsonWriterOptions writerOptions = default,
                int bufferSize = JsonSerializerOptions.BufferSizeDefault)
            {
                var bufferWriter = new PooledByteBufferWriter(bufferSize);
                using var writer = new Utf8JsonWriter(bufferWriter, writerOptions);
                node.WriteTo(writer, options);
                return bufferWriter;
            }
        }

        internal TypeToConvert ConvertJsonElement<TypeToConvert>()
        {
            JsonElement element = (JsonElement)(object)Value!;

            switch (element.ValueKind)
            {
                case JsonValueKind.Number:
                    if (typeof(TypeToConvert) == typeof(int) || typeof(TypeToConvert) == typeof(int?))
                    {
                        return (TypeToConvert)(object)element.GetInt32();
                    }

                    if (typeof(TypeToConvert) == typeof(long) || typeof(TypeToConvert) == typeof(long?))
                    {
                        return (TypeToConvert)(object)element.GetInt64();
                    }

                    if (typeof(TypeToConvert) == typeof(double) || typeof(TypeToConvert) == typeof(double?))
                    {
                        return (TypeToConvert)(object)element.GetDouble();
                    }

                    if (typeof(TypeToConvert) == typeof(short) || typeof(TypeToConvert) == typeof(short?))
                    {
                        return (TypeToConvert)(object)element.GetInt16();
                    }

                    if (typeof(TypeToConvert) == typeof(decimal) || typeof(TypeToConvert) == typeof(decimal?))
                    {
                        return (TypeToConvert)(object)element.GetDecimal();
                    }

                    if (typeof(TypeToConvert) == typeof(byte) || typeof(TypeToConvert) == typeof(byte?))
                    {
                        return (TypeToConvert)(object)element.GetByte();
                    }

                    if (typeof(TypeToConvert) == typeof(float) || typeof(TypeToConvert) == typeof(float?))
                    {
                        return (TypeToConvert)(object)element.GetSingle();
                    }

                    if (typeof(TypeToConvert) == typeof(uint) || typeof(TypeToConvert) == typeof(uint?))
                    {
                        return (TypeToConvert)(object)element.GetUInt32();
                    }

                    if (typeof(TypeToConvert) == typeof(ushort) || typeof(TypeToConvert) == typeof(ushort?))
                    {
                        return (TypeToConvert)(object)element.GetUInt16();
                    }

                    if (typeof(TypeToConvert) == typeof(ulong) || typeof(TypeToConvert) == typeof(ulong?))
                    {
                        return (TypeToConvert)(object)element.GetUInt64();
                    }

                    if (typeof(TypeToConvert) == typeof(sbyte) || typeof(TypeToConvert) == typeof(sbyte?))
                    {
                        return (TypeToConvert)(object)element.GetSByte();
                    }
                    break;

                case JsonValueKind.String:
                    if (typeof(TypeToConvert) == typeof(string))
                    {
                        return (TypeToConvert)(object)element.GetString()!;
                    }

                    if (typeof(TypeToConvert) == typeof(DateTime) || typeof(TypeToConvert) == typeof(DateTime?))
                    {
                        return (TypeToConvert)(object)element.GetDateTime();
                    }

                    if (typeof(TypeToConvert) == typeof(DateTimeOffset) || typeof(TypeToConvert) == typeof(DateTimeOffset?))
                    {
                        return (TypeToConvert)(object)element.GetDateTimeOffset();
                    }

                    if (typeof(TypeToConvert) == typeof(Guid) || typeof(TypeToConvert) == typeof(Guid?))
                    {
                        return (TypeToConvert)(object)element.GetGuid();
                    }

                    if (typeof(TypeToConvert) == typeof(char) || typeof(TypeToConvert) == typeof(char?))
                    {
                        string? str = element.GetString();
                        Debug.Assert(str != null);
                        if (str.Length == 1)
                        {
                            return (TypeToConvert)(object)str[0];
                        }
                    }
                    break;

                case JsonValueKind.True:
                case JsonValueKind.False:
                    if (typeof(TypeToConvert) == typeof(bool) || typeof(TypeToConvert) == typeof(bool?))
                    {
                        return (TypeToConvert)(object)element.GetBoolean();
                    }
                    break;
            }

            throw new InvalidOperationException(SR.Format(SR.NodeUnableToConvertElement,
                element.ValueKind,
                typeof(TypeToConvert)));
        }

        internal bool TryConvertJsonElement<TypeToConvert>([NotNullWhen(true)] out TypeToConvert result)
        {
            bool success;

            JsonElement element = (JsonElement)(object)Value!;

            switch (element.ValueKind)
            {
                case JsonValueKind.Number:
                    if (typeof(TypeToConvert) == typeof(int) || typeof(TypeToConvert) == typeof(int?))
                    {
                        success = element.TryGetInt32(out int value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(long) || typeof(TypeToConvert) == typeof(long?))
                    {
                        success = element.TryGetInt64(out long value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(double) || typeof(TypeToConvert) == typeof(double?))
                    {
                        success = element.TryGetDouble(out double value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(short) || typeof(TypeToConvert) == typeof(short?))
                    {
                        success = element.TryGetInt16(out short value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(decimal) || typeof(TypeToConvert) == typeof(decimal?))
                    {
                        success = element.TryGetDecimal(out decimal value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(byte) || typeof(TypeToConvert) == typeof(byte?))
                    {
                        success = element.TryGetByte(out byte value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(float) || typeof(TypeToConvert) == typeof(float?))
                    {
                        success = element.TryGetSingle(out float value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(uint) || typeof(TypeToConvert) == typeof(uint?))
                    {
                        success = element.TryGetUInt32(out uint value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(ushort) || typeof(TypeToConvert) == typeof(ushort?))
                    {
                        success = element.TryGetUInt16(out ushort value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(ulong) || typeof(TypeToConvert) == typeof(ulong?))
                    {
                        success = element.TryGetUInt64(out ulong value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(sbyte) || typeof(TypeToConvert) == typeof(sbyte?))
                    {
                        success = element.TryGetSByte(out sbyte value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }
                    break;

                case JsonValueKind.String:
                    if (typeof(TypeToConvert) == typeof(string))
                    {
                        string? strResult = element.GetString();
                        Debug.Assert(strResult != null);
                        result = (TypeToConvert)(object)strResult;
                        return true;
                    }

                    if (typeof(TypeToConvert) == typeof(DateTime) || typeof(TypeToConvert) == typeof(DateTime?))
                    {
                        success = element.TryGetDateTime(out DateTime value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(DateTimeOffset) || typeof(TypeToConvert) == typeof(DateTimeOffset?))
                    {
                        success = element.TryGetDateTimeOffset(out DateTimeOffset value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(Guid) || typeof(TypeToConvert) == typeof(Guid?))
                    {
                        success = element.TryGetGuid(out Guid value);
                        result = (TypeToConvert)(object)value;
                        return success;
                    }

                    if (typeof(TypeToConvert) == typeof(char) || typeof(TypeToConvert) == typeof(char?))
                    {
                        string? str = element.GetString();
                        Debug.Assert(str != null);
                        if (str.Length == 1)
                        {
                            result = (TypeToConvert)(object)str[0];
                            return true;
                        }
                    }
                    break;

                case JsonValueKind.True:
                case JsonValueKind.False:
                    if (typeof(TypeToConvert) == typeof(bool) || typeof(TypeToConvert) == typeof(bool?))
                    {
                        result = (TypeToConvert)(object)element.GetBoolean();
                        return true;
                    }
                    break;
            }

            result = default!;
            return false;
        }

        [ExcludeFromCodeCoverage] // Justification = "Design-time"
        [DebuggerDisplay("{Json,nq}")]
        private sealed class DebugView
        {
            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            public JsonValue<TValue> _node;

            public DebugView(JsonValue<TValue> node)
            {
                _node = node;
            }

            public string Json => _node.ToJsonString();
            public string Path => _node.GetPath();
            public TValue? Value => _node.Value;
        }
    }
}
